In [208]:
from vincent import *
The top-most element of Vega JSON documents is implemented by the Visualization
class. Visualizations contain all information for rendering a complete document.
We'll start by creating a new Visualization
.
In [209]:
vis = Visualization(width=400, height=200)
The keywords width
and height
are Vega document keys. All valid Vega keys can be specified as keywords in the class constructors. The keys are implemented as class properties - complete with doc strings - and can be explored via help (and tab-completion in ipython).
In [210]:
help(Visualization)
This will already dump some JSON, but there isn't enough yet to define a valid Vega document:
In [211]:
print(vis.to_json())
Vincent only creates JSON data for keys that have been explicitly defined. For example, though the property padding
already exists,
In [212]:
hasattr(vis, 'padding')
Out[212]:
it doesn't show up in the JSON output until it's been assigned. Similarly, Vega properties that have been assigned can also be deleted, which will prevent them from appearing in the encoded JSON. For example,
In [213]:
del vis.width
print(vis.to_json())
has removed "width"
from the returned JSON. We'd like our width defined though, so let's put it back.
In [214]:
vis.width = 400
We can add margins around the edges by setting the padding
property to a 4-element dict:
In [215]:
vis.padding = {'top': 10, 'left': 30, 'bottom': 20, 'right': 10}
print(vis.to_json())
Because valid JSON does not make valid Vega, Vincent tries to prevent us from generating documents that won't render figures in the client (usually the browser). For example, setting the padding
property with an invalid 'above'
key raises a ValueError
.
In [216]:
try:
vis.padding = {'above': 10, 'left': 30, 'bottom': 20, 'right': 10}
except ValueError as e:
print(e)
Vincent always raises ValueError
if it thinks a property assignment isn't valid Vega. Otherwise we might spend quite some time debugging our documents in the client.
Next we'll look at defining document data.
Data in Vega is defined in tabular form. Though it's certainly possible to set the data manually, it's much easier to use the class methods Data.from_iters
, Data.from_pandas
, and Data.from_numpy
.
In [217]:
mydata = Data.from_mult_iters(x=['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'], y=[28, 55, 43, 91, 81, 53, 19, 87, 52], idx='x')
print(mydata.to_json(pretty_print=False))
Note that in Vega, all data must have a name. Vincent will set the data name to 'table'
if it isn't provided.
Let's add this data to our visualization:
In [218]:
vis.data.append(mydata)
The data in the Vega JSON document is an array, allowing us to define multiple data sets in one document. The data elements (as well as scales and axes) can be accessed by their location,
In [219]:
vis.data[0].values[0]
Out[219]:
or by their name,
In [220]:
vis.data['table'].values[0]
Out[220]:
Assignments by name are also possible.
In [221]:
vis.data['table'] = mydata
In this case the key of the data ('table'
) must match the name
property of the assigned object, or a ValidationError
is raised. Data can also point to an external source by setting the url
property. See the Vega documentation for more info.
Let's add some scales to our visualization.
Scales map the data from the space of the data (the domain of the scale) to the space of the visualization (the range of the scale). These are defined using the Scale
class.
In [222]:
vis.scales['x'] = Scale(name='x', type='ordinal', range='width')
The domain of the scales is usually defined by referencing a field of the data. These reference are defined by the DataRef
class.
In [223]:
vis.scales['x'].domain = DataRef(data='table', field='data.idx')
vis.scales['x'].to_json(pretty_print=False)
Out[223]:
Our x-data is ordinal (since it's letters), but our y-data is quantitative. If the type
property is undefined, Vega defaults to a linear
type.
In [224]:
vis.scales.append(Scale(name='y', range='height', nice=True, domain=DataRef(data='table', field='data.val')))
Now let's add some axes to define the visualization space.
Axes provide a guide for translating spatial relationships about data. They are defined, naturally, by the Axis
class.
In [225]:
vis.axes.extend([Axis(type='x', scale='x'), Axis(type='y', scale='y')])
While Vega generally doesn't care if we label our data 'x'
or 'y'
(as opposed to, say, 'time'
), the axes are the exception. Here, 'x'
is always used to refer to the horizontal axis, while 'y'
refers to the vertical axis. Also note that while the data
and scales
properties are keyed according to the name
property of their contents, the axes
property is keyed according to the type
property.
Marks are the most fundamental part of the visualization; they're what the viewer sees. All marks in Vega have a type
, such as line
, rect
, ect. To add the bars to our bar chart, we add rect
marks:
In [226]:
bars = Mark(type='rect')
We define the data set the Marks represent by assigning the from_
property to a MarkRef
:
In [227]:
bars.from_ = MarkRef(data='table')
Though from_
is used for the property name - from
would be invalid Python syntax - the JSON field is still "from"
:
In [228]:
bars.to_json(pretty_print=False)
Out[228]:
The appearance of the marks are determined by the properties
property. The properties
is set to a MarkProperties
object that has enter
, exit
, hover
, and update
, which correspond to different events that may alter the mark's appearance. Each property of the MarkProperties
object can be set to a PropertySet
class that specifies the appearance details, such as color, stroke, etc.. Finally, each property of PropertySet is set to a ValueRef
class. The ValueRef
s link the details of the mark's appearance to the data via the scales.
This might sound a bit complicated, because it is. Maintaining the flexibility of something like a visualization grammar requires multiple levels of abstraction.
In [229]:
bars.properties = MarkProperties()
bars.properties.enter = PropertySet()
# Set the x-location of the bars to the data's x field, mapped through the x scale. Internally vincent tidies the data to 'idx'
bars.properties.enter.x = ValueRef(scale='x', field='data.idx')
# Use "band" to set the width of the bars to be flush against one another, minus a 1-pixel offset.
bars.properties.enter.width = ValueRef(scale='x', band=True, offset=-1)
# Set the height of the bars to the data's y field, mapped through the y scale.Internally vincent tidies the data to 'val'
bars.properties.enter.y = ValueRef(scale='y', field='data.val')
# Set the bottom of the bars to the x-axis.
bars.properties.enter.y2 = ValueRef(scale='y', value=0)
bars.properties.update = PropertySet()
# On the "update" event (see the Vega docs for details), set the color of the bars to "steelblue".
bars.properties.update.fill = ValueRef(value='steelblue')
# On the "hover" event, set the color of the bars to "red".
bars.properties.hover = PropertySet()
bars.properties.hover.fill = ValueRef(value='red')
As one might imagine, some fairly complex, dynamic marks can be defined. Use your imagination.
Now adding the marks to the visualization,
In [230]:
vis.marks.append(bars)
Finally, visualizing it in the Notebook:
In [231]:
import vincent
vincent.core.initialize_notebook()
vis.display()
Once the visualization is defined, it's fairly easy to change it's properties:
In [234]:
vis.marks[0].properties.hover.fill.value = 'gray'
vis.width = 800
vis.display()